/* This file is part of swapper project
 *
 * Copyright (C) 2020 The Swapper Project Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package com.swapper.mock.bean;


import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public abstract class QuickDirtyBean {

  @SuppressWarnings("all")
  @Override
  public boolean equals(Object o) {
    return ReflectionHelper.equals(this, o);
  }

  @Override
  public int hashCode() {
    return ReflectionHelper.hashCode(this);
  }

  @Override
  public String toString() {
    return ReflectionHelper.toString(this);
  }

  private static final class ReflectionHelper {
    private ReflectionHelper() {
      throw new UnsupportedOperationException(getClass().getSimpleName());
    }

    public static <T> boolean equals(Object a, Object b) {
      if (a == b) return true;
      if (a == null || b == null) return false;
      Class<?> clazz = a.getClass();
      if (clazz != b.getClass()) return false;
      if (clazz.isArray()) {
        int length = Array.getLength(a);
        if (length != Array.getLength(b)) return false;
        for (int i = 0; i < length; ++i) {
          if (!equals(Array.get(a, i), Array.get(b, i))) {
            return false;
          }
        }
        return true;
      }
      // If the class overrides the 'equals' method, it is called directly.
      try {
        Class<?> root = clazz.getMethod("equals", Object.class).getDeclaringClass();
        if (!Object.class.equals(root) && !QuickDirtyBean.class.equals(root)) {
          return a.equals(b);
        }
      } catch (NoSuchMethodException e) {
        // do nothing
      }
      List<Field> fields = Arrays.stream(clazz.getDeclaredFields())
        .filter(field -> !Modifier.isStatic(field.getModifiers()))
        .collect(Collectors.toList());
      for (Field field : fields) {
        field.setAccessible(true);
        try {
          if (!equals(field.get(a), field.get(b))) {
            return false;
          }
        } catch (IllegalAccessException e) {
          e.printStackTrace();
        }
      }
      return true;
    }

    public static <T> int hashCode(Object o) {
      if (o == null) return 0;
      Class<?> clazz = o.getClass();
      if (clazz.isArray()) {
        int result = 1;
        int length = Array.getLength(o);
        for (int i = 0; i < length; ++i) {
          result = 31 * result + hashCode(Array.get(o, i));
        }
        return result;
      }
      // If the class overrides the 'hashCode' method, it is called directly.
      try {
        Class<?> root = clazz.getMethod("hashCode").getDeclaringClass();
        if (!Object.class.equals(root) && !QuickDirtyBean.class.equals(root)) {
          return o.hashCode();
        }
      } catch (NoSuchMethodException e) {
        // do nothing
      }
      int result = 1;
      List<Field> fields = Arrays.stream(clazz.getDeclaredFields())
        .filter(field -> !Modifier.isStatic(field.getModifiers()))
        .collect(Collectors.toList());
      for (Field field : fields) {
        field.setAccessible(true);
        try {
          result = 31 * result + hashCode(field.get(o));
        } catch (IllegalAccessException e) {
          e.printStackTrace();
        }
      }
      return result;
    }

    public static <T> String toString(Object o) {
      if (o == null) return "null";
      Class<?> clazz = o.getClass();
      if (clazz.isArray()) {
        int iMax = Array.getLength(o) - 1;
        if (iMax == -1) return "[]";
        StringBuilder builder = new StringBuilder();
        builder.append('[');
        for (int i = 0; ; ++i) {
          builder.append(toString(Array.get(o, i)));
          if (i == iMax)
            return builder.append(']').toString();
          builder.append(", ");
        }
      }
      // If the class overrides the toString method, it is called directly.
      try {
        Class<?> root = clazz.getMethod("toString").getDeclaringClass();
        if (!Object.class.equals(root) && !QuickDirtyBean.class.equals(root)) {
          return o.toString();
        }
      } catch (NoSuchMethodException e) {
        // do nothing
      }
      Field[] fields = Arrays.stream(clazz.getDeclaredFields())
        .filter(field -> !Modifier.isStatic(field.getModifiers()))
        .toArray(Field[]::new);
      int iMax = fields.length - 1;
      if (iMax == -1) return o.toString();
      StringBuilder builder = new StringBuilder();
      builder.append(clazz.getSimpleName()).append('{');
      for (int i = 0; ; ++i) {
        Field field = fields[i];
        field.setAccessible(true);
        builder.append(field.getName()).append("=");
        try {
          builder.append(toString(field.get(o)));
        } catch (IllegalAccessException e) {
          e.printStackTrace();
        }
        if (i == iMax)
          return builder.append('}').toString();
        builder.append(", ");
      }
    }
  }
}
